/*
Copyright 2014 Prasanna Thirumalai
This file is part of StackX.
StackX is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
StackX is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with StackX. If not, see <http://www.gnu.org/licenses/>.
*/
package com.prasanna.android.stacknetwork.utils;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import org.apache.http.protocol.HTTP;
import org.htmlcleaner.CleanerTransformations;
import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.SimpleHtmlSerializer;
import org.htmlcleaner.TagNode;
import org.htmlcleaner.TagTransformation;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.prasanna.android.http.HttpContentTypes;
import com.prasanna.android.stacknetwork.FullscreenTextActivity;
import com.prasanna.android.stacknetwork.R;
import com.prasanna.android.task.AsyncTaskCompletionNotifier;
import com.prasanna.android.task.GetImageAsyncTask;
import com.prasanna.android.utils.LogWrapper;
import com.prasanna.android.views.QuickActionMenu;
public class MarkdownFormatter {
private static final String BASE_URL = "file:///android_asset/google_code_prettify/prettify.js";
private static final String CODE_HTML_PREFIX = "<html><head>"
+ "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">"
+ "<link href=\"prettify_no_line_nums.css\" type=\"text/css\" rel=\"stylesheet\" />"
+ "<script type=\"text/javascript\" src=\"prettify.js\"></script>" + "<title>Insert title here</title>"
+ "</head>" + "<body onload=\"prettyPrint();\" bgcolor=\"#F5F4F2\">" + "<pre class=\"prettyprint linenums\">";
private static final String CODE_HTML_SUFFIX = "</pre></body></html>";
public static void loadText(final WebView webView, final String text) {
webView.setWebChromeClient(new WebChromeClient());
webView.setWebViewClient(new WebViewClient());
webView.getSettings().setJavaScriptEnabled(true);
webView.loadDataWithBaseURL(BASE_URL, CODE_HTML_PREFIX + MarkdownFormatter.escapeHtml(text) + CODE_HTML_SUFFIX,
HttpContentTypes.TEXT_HTML, HTTP.UTF_8, null);
}
private static final String TAG = MarkdownFormatter.class.getSimpleName();
private static final String NEW_LINE = System.getProperty("line.separator");
private static final String CR = "\r";
public static class Tags {
public static final String CODE = "code";
public static final String IMG = "img";
public static final String BR = "br";
}
public static class Attributes {
public static final String SRC = "src";
}
private static class AsyncTaskCompletionNotifierImagePopup implements AsyncTaskCompletionNotifier<Bitmap> {
private final Context context;
private Dialog progressDialog;
public AsyncTaskCompletionNotifierImagePopup(Context context, Dialog progressDialog) {
this.context = context;
this.progressDialog = progressDialog;
}
@Override
public void notifyOnCompletion(Bitmap result) {
if (progressDialog != null) progressDialog.dismiss();
ImageView imageView = new ImageView(context);
imageView.setImageBitmap(result);
showImageDialog(context, imageView);
}
private void showImageDialog(final Context context, ImageView imageView) {
AlertDialog.Builder imageDialog = new AlertDialog.Builder(context);
imageDialog.setView(imageView);
String hide = context.getResources().getString(R.string.hide);
imageDialog.setPositiveButton(hide, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
if (!((Activity) context).isFinishing()) imageDialog.show();
}
};
public static String escapeHtml(CharSequence text) {
if (text == null) return null;
StringBuilder builder = new StringBuilder();
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c == '<') builder.append("<");
else if (c == '>') builder.append(">");
else if (c == '&') builder.append("&");
else builder.append(c);
}
return builder.toString();
}
private static String clean(String markdownText) throws IOException {
HtmlCleaner cleaner = new HtmlCleaner();
CleanerTransformations transformations = new CleanerTransformations();
transformations.addTransformation(new TagTransformation(Tags.BR, Tags.BR + "/", true));
cleaner.setTransformations(transformations);
TagNode node = cleaner.clean(markdownText);
SimpleHtmlSerializer serializer = new SimpleHtmlSerializer(cleaner.getProperties());
serializer.write(node, new StringWriter(), HTTP.UTF_8);
return serializer.getAsString(node);
}
/**
* Format HTML text to fit into one or more vertically aligned text views.
* Parses the given text and removes {@code <code> </code>} tags. If the code
* text is of multiple lines a new {@link android.widget.TextView TextView} is
* created and added to the view container else the code text is added to
* already created {@link android.widget.TextView TextView}.
*
* @param context
* @param markdownText
* @return
*/
public static ArrayList<View> parse(Context context, String markdownText) {
if (context != null && markdownText != null) {
ArrayList<View> views = new ArrayList<View>();
try {
markdownText = clean(markdownText);
XmlPullParserFactory xmlPullParserFactory = XmlPullParserFactory.newInstance();
XmlPullParser xmlPullParser = xmlPullParserFactory.newPullParser();
xmlPullParser.setInput(new StringReader(markdownText));
int eventType = xmlPullParser.getEventType();
StringBuffer buffer = new StringBuffer();
StringBuffer code = new StringBuffer();
boolean codeFound = false;
boolean oneLineCode = false;
LinearLayout.LayoutParams params =
new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_DOCUMENT) {
} else if (eventType == XmlPullParser.START_TAG) {
if (xmlPullParser.getName().equals(Tags.CODE)) codeFound = true;
else if (xmlPullParser.getName().equals(Tags.IMG)) {
addSimpleTextToView(context, views, buffer, params);
String attributeValue = xmlPullParser.getAttributeValue(null, Attributes.SRC);
addImgLinkText(context, views, attributeValue, params);
} else {
buffer.append("<" + xmlPullParser.getName());
for (int i = 0; i < xmlPullParser.getAttributeCount(); i++) {
buffer.append(" " + xmlPullParser.getAttributeName(i) + "=\"" + xmlPullParser.getAttributeValue(i)
+ "\"");
}
buffer.append(">");
}
} else if (eventType == XmlPullParser.END_TAG) {
if (xmlPullParser.getName().equals(Tags.CODE)) {
codeFound = false;
if (oneLineCode) oneLineCode = false;
else {
addSimpleTextToView(context, views, buffer, params);
views.add(getTextViewForCode(context, code.toString()));
code.delete(0, code.length());
}
} else if (xmlPullParser.getName().equals(Tags.IMG)) {
LogWrapper.v(TAG, "Ignore img tag");
} else {
buffer.append("</" + xmlPullParser.getName() + ">");
}
} else if (eventType == XmlPullParser.TEXT) {
String text = xmlPullParser.getText();
if (codeFound) {
if (!text.contains(NEW_LINE)) {
if (buffer.length() > 0 && buffer.lastIndexOf(NEW_LINE) == buffer.length() - 1)
buffer.setCharAt(buffer.length() - 1, ' ');
buffer.append(text);
oneLineCode = true;
} else {
code.append(text);
}
} else {
text = text.replace(NEW_LINE, " ").replace(CR, " ");
buffer.append(text);
}
}
eventType = xmlPullParser.next();
}
addSimpleTextToView(context, views, buffer, params);
} catch (XmlPullParserException e) {
LogWrapper.e(TAG, "Error parsing: " + e);
} catch (IOException e) {
LogWrapper.e(TAG, "Error parsing: " + e);
}
return views;
}
return null;
}
private static void addSimpleTextToView(Context context, ArrayList<View> views, StringBuffer buffer,
LinearLayout.LayoutParams params) {
if (buffer.length() > 0) {
views.add(getTextView(context, params, buffer));
buffer.delete(0, buffer.length());
}
}
private static void addImgLinkText(final Context context, ArrayList<View> views, final String url,
LinearLayout.LayoutParams params) {
final TextView textView = new TextView(context);
textView.setTextColor(Color.BLUE);
textView.setLayoutParams(params);
textView.setText("View image");
textView.setTextSize(getTextSize(context));
textView.setPadding(3, 3, 3, 3);
textView.setTag(url);
textView.setClickable(true);
setupOnLinkClick(context, url, textView);
views.add(textView);
}
private static void setupOnLinkClick(final Context context, final String url, final TextView textView) {
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
AlertDialog.Builder imageDialog = new AlertDialog.Builder(context);
ProgressBar progressBar = new ProgressBar(context);
imageDialog.setView(progressBar);
AlertDialog progressDialog = imageDialog.create();
progressDialog.show();
new GetImageAsyncTask(new AsyncTaskCompletionNotifierImagePopup(context, progressDialog)).execute(url);
}
});
}
private static TextView getTextView(Context context, LinearLayout.LayoutParams params, StringBuffer buffer) {
TextView textView = new TextView(context);
textView.setTextColor(Color.BLACK);
textView.setLayoutParams(params);
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setTextSize(getTextSize(context));
textView.setText(Html.fromHtml(buffer.toString()));
return textView;
}
private static float getTextSize(Context context) {
float textSize = 12f;
int screentLayoutSize =
context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
if (screentLayoutSize == Configuration.SCREENLAYOUT_SIZE_LARGE
|| screentLayoutSize == Configuration.SCREENLAYOUT_SIZE_XLARGE) textSize = 15f;
return textSize;
}
private static RelativeLayout getTextViewForCode(final Context context, final String text) {
final LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final RelativeLayout codeLayout = (RelativeLayout) inflater.inflate(R.layout.code, null);
final WebView webView = (WebView) codeLayout.findViewById(R.id.code);
loadText(webView, text);
StackXQuickActionMenu quickActionMenu =
new StackXQuickActionMenu(context).addEmailQuickActionItem("Code sample from StackX", text)
.addCopyToClipboardItem(text);
final QuickActionMenu actionMenu = quickActionMenu.addItem(R.string.fullscreen, new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(context, FullscreenTextActivity.class);
intent.putExtra(StringConstants.TEXT, text);
context.startActivity(intent);
}
}).build();
ImageView imageView = (ImageView) codeLayout.findViewById(R.id.codeQuickActionMenu);
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
actionMenu.show(v);
}
});
return codeLayout;
}
};